<?php
###
#  Baing PHP 框架 1.0.0
#  http://baing.co/
#
#  Copyright: 白晓阳
#  License: CC BY-NC-SA 4.0
#  https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh
###

namespace bai\action;

use bai\resource\document\DataTable;


class DataQuery extends \bai\Action
{
    public const HOW = 'HOW';
    public const WHEN = 'WHEN';
    public const PAGE = 20;

    protected ?DataTable $resource = null;
    protected array $params = [];

    public function run()
    {
        $method = $this->runtime['method'];
        if ($method === self::DELETE && $this->resource->findSoftDelete()) {
            $method = self::PATCH;
        }

        $sql = $this->find(['SQL', $method]);
        $sql = preg_replace_callback('/:([a-zA-Z0-9]+)/', function($matches) {
            $func = 'prepare' . ucfirst($matches[1]);
            if (method_exists($this, $func)) {
                return $this->$func();
            }
            return '';
        }, $sql);

        $context = [
            'sql' => $sql,
            'params' => $this->params
        ];
        return $this->next($context);
    }

    protected function init()
    {
        $this->runtime['method'] ??= self::$request->method;
        $resourceName = $this->find('resource') ?? self::$process->resource;
        $this->resource = DataTable::setup(['path' => $resourceName]);
        $this->resource->run();
    }

    protected function prepareTable(): string
    {
        return $this->resource->name;
    }

    /**
     * Fields:
     * 'field1'
     * ['field1', 'field2', ...]
     */
    protected function prepareFields(): string
    {
        $fields = $this->find('fields') ?? self::$request->fields;
        $filtered = $this->resource->filterFields($fields);

        $items = [];
        foreach ($filtered as $name => $def) {
            if (!isset($def['name']) || $def['name'] === $name) {
                $items[] = $name;
                continue;
            }
            $items[] = "{$def['name']} AS $name";
        }
        return join(', ', $items);
    }

    /**
     * Query:
     * [
     *      'field1' => 'value1'
     *      'field2' => ['value1', 'value2', ...]
     *      'field3' => ['value' => 'value3', 'WHEN' => '>']
     *      'field4' => [
     *          ':HOW' => 'OR'
     *          'field5' => 'value1'
     *          'field6' => ['value' => ['value2', 'value3'], 'WHEN' => 'BETWEEN']
     *      ]
     * ]
     */
    protected function prepareQuery(array $query = null): string
    {
        $method = $this->find('method');
        $query ??= $this->find('query') ?? self::$request->query ?? [];
        $filtered = $this->resource->filterQuery($query, $method);

        $items = [];
        foreach ($filtered as $name => $value) {
            if ($item = $this->prepareQueryField($name, $value)) {
                $items[] = $item;
            }
        }

        $options = $this->find(self::HOW);
        $how = $this->checkOption($query[self::HOW] ?? '', $options);
        if ($how === 'NOT') {
            return "$how (" . join(" $options[0] ", $items) . ")";
        }
        return "(" . join(" $how ", $items) . ")";
    }

    /**
     * Data:
     * [
     *      'field1' => 'value1',
     *      'field2' => ['value1', 'value2', ...],
     * ]
     */
    protected function prepareData(): string
    {
        $method = $this->find('method');
        $data = $this->find('data') ?? self::$request->data ?? [];
        $filtered = $this->resource->filterData($data, $method);

        $items = [];
        foreach ($filtered as $name => $value) {
            if ($item = $this->prepareDataField($name, $value)) {
                $items[] = $item;
            }
        }
        return join(', ', $items);
    }

    /**
     * Quality:
     * [
     *     GROUP => 'field1'
     *     GROUP => ['field1', 'field2', ...]
     *     ORDER => 'field1' | 'field1 DESC'
     *     ORDER => ['field1' => 'ASC', 'field2' => 'DESC', ...]
     *     PAGE => 3
     * ]
     */
    protected function prepareQuality()
    {
        $quality = $this->find('quality') ?? self::$request->quality ?? [];
        $filtered = $this->resource->filterQuality($quality);
        if (!$filtered) return '';

        $items = [];
        if ($order = $filtered['ORDER'] ?? '') {
            $options = $this->find('ORDER');
            if (is_string($order)) {
                $order = explode(' ', trim($order));
                if (count($order) < 2) {
                    $order = [$order[0] => $options[0]];
                } else {
                    $order = [$order[0] => $this->checkOption(end($order), $options)];
                }
            }
            if (is_array($order)) {
                $fields = $this->resource->filterFields(array_keys($order), []);
                if ($fields) {
                    array_walk($fields, fn(&$def, $key) => ($def = "$key " . $this->checkOption($order[$key], $options)));
                    $order = join(', ', $fields);
                    $items[] = str_replace(':order', $order, $this->find('SQL.ORDER'));
                }
            }
        }
        if ($group = $filtered['GROUP'] ?? '') {
            if (is_string($group)) {
                $group = [trim($group)];
            }
            $fields = $this->resource->filterFields($group, []);
            if ($fields) {
                $group = join(', ', array_keys($fields));
                $items[] = str_replace(':group', $group, $this->find('SQL.GROUP'));
            }
        }
        if ($page = $filtered['PAGE'] ?? 0) {
            $limit = $this->find('PAGE') ?? self::PAGE;
            $offset = $limit * ($page - 1);
            $items[] = str_replace([':limit', ':offset'], [$limit, $offset], $this->find('SQL.LIMIT'));
        } else if ($limit = $filtered['LIMIT'] ?? 0) {
            $items[] = str_replace([':limit', ':offset'], [$limit, 0], $this->find('SQL.LIMIT'));
        }
        return join(' ', $items);
    }

    protected function prepareQueryField(string $field, $value): string
    {
        $field = $this->resource->findFieldName($field);
        if (!$field) return '';
        if ($value === null) {
            return "$field IS NULL";
        }
        if (is_scalar($value)) {
            $param = $this->prepareParam($field, $value);
            return "$field = $param";
        }
        if (!is_array($value)) {
            return '';
        }

        if (isset($value[self::HOW])) {
            return $this->prepareQuery($value);
        }

        $options = $this->find(self::WHEN);
        if (!isset($value[self::WHEN])) {
            $params = $this->prepareParam($field, $value);
            $params = join(', ', $params);
            return "$field $options[0] ($params)";
        }

        $when = $this->checkOption($value[self::WHEN], $options);
        if (!isset($value['value'])) {
            return "$field $when";
        }
        if (is_scalar($value['value'])) {
            $paramValue = $value['value'];
            if ($when === 'LIKE' || $when === 'NOT LIKE') {
                $paramValue = "%{$paramValue}%";
            }
            $param = $this->prepareParam($field, $paramValue);
            return "$field $when $param";
        }
        if (!is_array($value['value'])) {
            return '';
        }

        if ($when === 'BETWEEN' || $when === 'NOT BETWEEN') {
            $params = [
                $this->prepareParam($field, reset($value['value'])),
                $this->prepareParam($field, end($value['value']))
            ];
            $params = join(' AND ', $params);
            return "$field $when $params";
        }

        $params = $this->prepareParam($field, $value);
        $params = join(', ', $params);
        return "$field $when ($params)";
    }

    protected function prepareDataField(string $field, $value): string
    {
        $field = $this->resource->findFieldName($field);
        if (!$field) return '';
        if ($value === null) {
            return "$field = NULL";
        }

        if (is_array($value)) {
            $value = join(',', str_replace(',', '', $value));
        }
        if (is_scalar($value)) {
            $param = $this->prepareParam($field, $value);
            return "$field = $param";
        }
        return '';
    }

    protected function prepareParam(string $field, $value)
    {
        if (!is_array($value)) {
            $key = '_' . count($this->params) . '_' . $field;
            $this->params[$key] = $value;
            return ':' . $key;
        }

        $params = [];
        foreach ($value as $item) {
            $param = $this->prepareParam($field, $item);
            if (is_array($param)) {
                $params = array_merge($params, $param);
                continue;
            }
            $params[] = $param;
        }
        return $params;
    }

    protected function checkOption(string $option, array $list)
    {
        $option = strtoupper($option);
        if (in_array($option, $list)) return $option;
        return $list[0];
    }
}
